﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ResolverBase.cs" company="Rolosoft Ltd">
//   © Rolosoft Ltd
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

#region License

// // Copyright 2013 Rolosoft Ltd
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// // http://www.apache.org/licenses/LICENSE-2.0
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.
#endregion

namespace Rsft.Net.Dns
{
    #region Usings

    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Globalization;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Text.RegularExpressions;

    using Rsft.Net.Dns.Diagnostics;
    using Rsft.Net.Dns.Entities;
    using Rsft.Net.Dns.Interfaces;

    #endregion

    /// <summary>
    ///     The resolver base class.
    /// </summary>
    public abstract class ResolverBase : IResolver
    {
        #region Fields
        
        /// <summary>
        ///     The cache factory.
        /// </summary>
        private readonly ICacheFactory cacheFactory;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ResolverBase"/> class.
        /// </summary>
        /// <param name="recordReaderFactory">
        /// The record reader factory.
        /// </param>
        /// <param name="cacheFactory">
        /// The cache factory.
        /// </param>
        /// <param name="logger">
        /// The logger.
        /// </param>
        protected ResolverBase(IRecordReaderFactory recordReaderFactory, ICacheFactory cacheFactory, ILogger logger)
        {
            Contract.Requires(recordReaderFactory != null);
            Contract.Requires(cacheFactory != null);
            Contract.Requires(logger != null);

            this.RecordReaderFactory = recordReaderFactory;
            this.cacheFactory = cacheFactory;
            this.Logger = logger;
        }

        #endregion

        #region Public Events

        /// <summary>
        ///     The verbose event.
        /// </summary>
        public event EventHandler<VerboseEventArgs> VerboseEvent;

        #endregion

        #region Public Properties

        /// <summary>
        ///     Gets or sets the configuration.
        /// </summary>
        /// <value>
        ///     The configuration.
        /// </value>
        /// <remarks>
        ///     Thread safe.
        /// </remarks>
        public abstract IConfiguration Configuration { get; set; }

        /// <summary>
        /// Gets the logger.
        /// </summary>
        /// <value>
        /// The logger.
        /// </value>
        protected ILogger Logger { get; private set; }

        /// <summary>
        /// Gets the record reader factory.
        /// </summary>
        /// <value>
        /// The record reader factory.
        /// </value>
        protected IRecordReaderFactory RecordReaderFactory { get; private set; }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets or sets the unique header id.
        /// </summary>
        /// <value>
        ///     The unique header id.
        /// </value>
        /// <remarks>
        ///     Thread safe.
        /// </remarks>
        protected abstract int UniqueHeaderId { get; set; }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// The begin get host addresses.
        /// </summary>
        /// <param name="hostNameOrAddress">
        /// The host name or address.
        /// </param>
        /// <param name="requestCallback">
        /// The request callback.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginGetHostAddresses(
            string hostNameOrAddress, AsyncCallback requestCallback, object value);

        /// <summary>
        /// The begin get host by address.
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="state">
        /// The state.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginGetHostByAddress(string address, AsyncCallback asyncCallback, object state);

        /// <summary>
        /// The begin get host by address.
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="state">
        /// The state.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginGetHostByAddress(IPAddress address, AsyncCallback asyncCallback, object state);

        /// <summary>
        /// The begin get host by name.
        /// </summary>
        /// <param name="hostName">
        /// The host name.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginGetHostByName(string hostName, AsyncCallback asyncCallback, object value);

        /// <summary>
        /// The begin get host entry.
        /// </summary>
        /// <param name="hostNameOrAddress">
        /// The host name or address.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginGetHostEntry(
            string hostNameOrAddress, AsyncCallback asyncCallback, object value);

        /// <summary>
        /// The begin get host entry.
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginGetHostEntry(IPAddress address, AsyncCallback asyncCallback, object value);

        /// <summary>
        /// Begins the get TCP request.
        /// </summary>
        /// <param name="request">
        /// The request.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="state">
        /// The state.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>
        /// </returns>
        public abstract IAsyncResult BeginGetTcpRequest(Request request, AsyncCallback asyncCallback, object state);

        /// <summary>
        /// Begins the get UDP request.
        /// </summary>
        /// <param name="request">
        /// The request.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="state">
        /// The state.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>
        /// </returns>
        public abstract IAsyncResult BeginGetUdpRequest(Request request, AsyncCallback asyncCallback, object state);

        /// <summary>
        /// The begin query.
        /// </summary>
        /// <param name="name">
        /// The name.
        /// </param>
        /// <param name="queryType">
        /// The query type.
        /// </param>
        /// <param name="queryClassType">
        /// The query class type.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="state">
        /// The state.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginQuery(
            string name, QType queryType, QClassType queryClassType, AsyncCallback asyncCallback, object state);

        /// <summary>
        /// The begin resolve.
        /// </summary>
        /// <param name="hostName">
        /// The host name.
        /// </param>
        /// <param name="asyncCallback">
        /// The async callback.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="IAsyncResult"/>.
        /// </returns>
        public abstract IAsyncResult BeginResolve(string hostName, AsyncCallback asyncCallback, object value);

        /// <summary>
        /// The end get host addresses.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="IPAddress[]"/>.
        /// </returns>
        public abstract IPAddress[] EndGetHostAddresses(IAsyncResult asyncResult);

        /// <summary>
        /// The end get host by address.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public abstract IPHostEntry EndGetHostByAddress(IAsyncResult asyncResult);

        /// <summary>
        /// The end get host by name.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public abstract IPHostEntry EndGetHostByName(IAsyncResult asyncResult);

        /// <summary>
        /// The end get host entry.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public abstract IPHostEntry EndGetHostEntry(IAsyncResult asyncResult);

        /// <summary>
        /// Ends the get TCP request.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="Response"/>.
        /// </returns>
        public abstract Response EndGetTcpRequest(IAsyncResult asyncResult);

        /// <summary>
        /// Ends the get UDP request.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="Response"/>.
        /// </returns>
        public abstract Response EndGetUdpRequest(IAsyncResult asyncResult);

        /// <summary>
        /// The end query.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="Response"/>.
        /// </returns>
        public abstract Response EndQuery(IAsyncResult asyncResult);

        /// <summary>
        /// The end resolve.
        /// </summary>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public abstract IPHostEntry EndResolve(IAsyncResult asyncResult);

        /// <summary>
        /// The get ARPA from ENUM.
        /// </summary>
        /// <param name="myEnum">
        /// The my ENUM.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public string GetArpaFromEnum(string myEnum)
        {
            var sb = new StringBuilder();
            var number = Regex.Replace(myEnum, "[^0-9]", string.Empty);
            sb.Append("e164.arpa.");
            foreach (char c in number)
            {
                sb.Insert(0, string.Format(CultureInfo.InvariantCulture, "{0}.", c));
            }

            return sb.ToString();
        }

        /// <summary>
        /// The get ARPA from IP.
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public string GetArpaFromIp(IPAddress address)
        {
            if (address.AddressFamily == AddressFamily.InterNetwork)
            {
                var sb = new StringBuilder();
                sb.Append("in-addr.arpa.");
                foreach (byte b in address.GetAddressBytes())
                {
                    sb.Insert(0, string.Format(CultureInfo.InvariantCulture, "{0}.", b));
                }

                return sb.ToString();
            }

            if (address.AddressFamily == AddressFamily.InterNetworkV6)
            {
                var sb = new StringBuilder();
                sb.Append("ip6.arpa.");
                foreach (byte b in address.GetAddressBytes())
                {
                    sb.Insert(0, string.Format(CultureInfo.InvariantCulture, "{0:x}.", (b >> 4) & 0xf));
                    sb.Insert(0, string.Format(CultureInfo.InvariantCulture, "{0:x}.", (b >> 0) & 0xf));
                }

                return sb.ToString();
            }

            return "?";
        }

        /// <summary>
        /// The get host addresses.
        /// </summary>
        /// <param name="hostNameOrAddress">
        /// The host name or address.
        /// </param>
        /// <returns>
        /// The <see cref="IPAddress[]"/>.
        /// </returns>
        public IPAddress[] GetHostAddresses(string hostNameOrAddress)
        {
            var entry = this.GetHostEntry(hostNameOrAddress);
            return entry.AddressList;
        }

        /// <summary>
        /// The get host by address.
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public IPHostEntry GetHostByAddress(IPAddress address)
        {
            return this.GetHostEntry(address);
        }

        /// <summary>
        /// The get host by address.
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public IPHostEntry GetHostByAddress(string address)
        {
            return this.GetHostEntry(address);
        }

        /// <summary>
        /// The get host by name.
        /// </summary>
        /// <param name="hostName">
        /// The host name.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public IPHostEntry GetHostByName(string hostName)
        {
            return this.MakeEntry(hostName);
        }

        /// <summary>
        /// The get host entry.
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public IPHostEntry GetHostEntry(IPAddress address)
        {
            var arpaFromIp = this.GetArpaFromIp(address);

            var response = this.Query(arpaFromIp, QType.PTR);

            return response.RecordsPtr.Count > 0 ? this.MakeEntry(response.RecordsPtr[0].PtrdName) : new IPHostEntry();
        }

        /// <summary>
        /// The get host entry.
        /// </summary>
        /// <param name="hostNameOrAddress">
        /// The host name or address.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public IPHostEntry GetHostEntry(string hostNameOrAddress)
        {
            IPAddress address;

            return IPAddress.TryParse(hostNameOrAddress, out address)
                       ? this.GetHostEntry(address)
                       : this.MakeEntry(hostNameOrAddress);
        }

        /// <summary>
        ///     Gets the host name of the local computer.
        /// </summary>
        /// <returns>
        ///     A string that contains the DNS host name of the local computer.
        /// </returns>
        /// <PermissionSet>
        ///     <IPermission
        ///         class="System.Net.DnsPermission, System, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        ///         version="1" Unrestricted="true" />
        /// </PermissionSet>
        public string GetHostName()
        {
            return Dns.GetHostName();
        }

        /// <summary>
        /// The query.
        /// </summary>
        /// <param name="name">
        /// The name.
        /// </param>
        /// <param name="queryType">
        /// The query type.
        /// </param>
        /// <param name="queryClassType">
        /// The query class type.
        /// </param>
        /// <returns>
        /// The <see cref="Response"/>.
        /// </returns>
        public Response Query(string name, QType queryType, QClassType queryClassType = QClassType.IN)
        {
            this.InitializeUniqueHeaderId();

            Question question = null;

            if (queryType == QType.PTR)
            {
                IPAddress tryAddress;
                if (IPAddress.TryParse(name, out tryAddress))
                {
                    var arpaFromIp = this.GetArpaFromIp(tryAddress);
                    question = new Question(arpaFromIp, queryClassType, queryType);
                }
                else
                {
                    question = new Question(name, queryClassType, queryType);    
                }
            }
            else
            {
                question = new Question(name, queryClassType, queryType);    
            }
            
            var response = this.SearchInCache(question);
            if (response != null)
            {
                return response;
            }

            var request = new Request();
            request.AddQuestion(question);
            var response1 = this.GetResponse(request);
            return response1;
        }

        /// <summary>
        /// The resolve.
        /// </summary>
        /// <param name="hostName">
        /// The host name.
        /// </param>
        /// <returns>
        /// The <see cref="IPHostEntry"/>.
        /// </returns>
        public IPHostEntry Resolve(string hostName)
        {
            return this.MakeEntry(hostName);
        }

        #endregion

        #region Methods

        /// <summary>
        /// The add to cache.
        /// </summary>
        /// <param name="response">
        /// The response.
        /// </param>
        protected void AddToCache(Response response)
        {
            Contract.Requires(response != null);

            var cache = this.cacheFactory.GetCache();

            // No question = no caching
            if (response.Questions.Count == 0)
            {
                return;
            }

            // Only cached non-error responses
            if (response.Header.ResponseCode != RCodeType.NoError)
            {
                return;
            }

            var question = response.Questions[0];

            var sb = new StringBuilder();
            sb.AppendFormat(
                CultureInfo.InvariantCulture, "{0}-{1}-{2}", question.QClassType, question.QType, question.QName);

            var strKey = sb.ToString();

            if (cache.Get(strKey) != null)
            {
                cache.Remove(strKey);
            }

            cache.Add(strKey, response);
        }

        /// <summary>
        ///     Initializes the unique header id.
        /// </summary>
        protected void InitializeUniqueHeaderId()
        {
            var next = new Random().Next(ushort.MinValue, ushort.MaxValue);
            this.UniqueHeaderId = next;
        }

        /// <summary>
        /// The search in cache.
        /// </summary>
        /// <param name="question">
        /// The question.
        /// </param>
        /// <returns>
        /// Various stuff.
        /// </returns>
        protected Response SearchInCache(Question question)
        {
            Contract.Requires(question != null);

            var cache = this.cacheFactory.GetCache();

            var sb = new StringBuilder();
            sb.AppendFormat(
                CultureInfo.InvariantCulture, "{0}-{1}-{2}", question.QClassType, question.QType, question.QName);

            var key = sb.ToString();

            // retrieve from  cache
            if (cache.Get(key) == null)
            {
                return null;
            }

            var response = (Response)cache.Get(key);

            // return null if TTL has expired.
            if (response != null)
            {
                var timeLived = (int)((DateTime.Now.Ticks - response.TimeStamp.Ticks) / TimeSpan.TicksPerSecond);
                foreach (ResourceRecord rr in response.RecordsResourceRecord)
                {
                    rr.TimeLived = timeLived;

                    // The TTL property calculates its actual time to live
                    if (rr.TimeToLive == 0)
                    {
                        return null; // out of date
                    }
                }
            }

            return response;
        }

        /// <summary>
        /// The TCP request.
        /// </summary>
        /// <param name="request">
        /// The request.
        /// </param>
        /// <returns>
        /// Various stuff.
        /// </returns>
        /// <exception cref="SocketException">
        /// Socket exception.
        /// </exception>
        protected abstract Response TcpRequest(Request request);

        /// <summary>
        /// The UDP request.
        /// </summary>
        /// <param name="request">
        /// The request.
        /// </param>
        /// <returns>
        /// Various stuff.
        /// </returns>
        protected abstract Response UdpRequest(Request request);

        /// <summary>
        /// The verbose.
        /// </summary>
        /// <param name="format">
        /// The format.
        /// </param>
        /// <param name="args">
        /// The args.
        /// </param>
        protected void Verbose(string format, params object[] args)
        {
            Contract.Requires(!string.IsNullOrEmpty(format));
            Contract.Requires(args != null);
            this.OnVerbose(new VerboseEventArgs(string.Format(CultureInfo.InvariantCulture, format, args)));
        }

        /// <summary>
        /// Gets the TTL.
        /// </summary>
        /// <param name="response">
        /// The response.
        /// </param>
        /// <returns>
        /// The TTL for the record
        /// </returns>
        private static uint GetTtl(Response response)
        {
            Contract.Requires(response != null);

            var recordsA = response.RecordsA as IEnumerable<Record>;

            var recordsMx = response.RecordsMX as IEnumerable<Record>;

            var ttlManagerA = new TTLManager<Record>(recordsA);
            var responseA = ttlManagerA.GetTTL();

            var ttlManagerMx = new TTLManager<Record>(recordsMx);
            var responseMx = ttlManagerMx.GetTTL();

            if (responseMx != ttlManagerMx.DefaultTTL)
            {
                return ttlManagerMx.GetTTL();
            }

            return responseA != ttlManagerA.DefaultTTL ? ttlManagerA.GetTTL() : ttlManagerA.DefaultTTL;
        }

        /// <summary>
        /// The get response.
        /// </summary>
        /// <param name="request">
        /// The request.
        /// </param>
        /// <returns>
        /// Various stuff.
        /// </returns>
        private Response GetResponse(Request request)
        {
            Contract.Requires(request != null);

            request.Header.Id = (ushort)this.UniqueHeaderId;
            request.Header.RecursionDesired = this.Configuration.Configuration.UseRecursion;

            if (this.Configuration.Configuration.TransportType == TransportType.Udp)
            {
                var udpResponse = this.UdpRequest(request);

                /*If truncation of response is detected*/
                if (udpResponse.Header.Truncation)
                {
                    /*Use TCP*/
                    var tcpResponse = this.TcpRequest(request);
                    return tcpResponse;
                }

                return udpResponse;
            }

            if (this.Configuration.Configuration.TransportType == TransportType.Tcp)
            {
                var tcpResponse = this.TcpRequest(request);
                return tcpResponse;
            }

            var response = new Response { Error = "Unknown TransportType" };
            return response;
        }

        /// <summary>
        /// The make entry.
        /// </summary>
        /// <param name="hostName">
        /// The host name.
        /// </param>
        /// <returns>
        /// Various stuff.
        /// </returns>
        private IPHostEntry MakeEntry(string hostName)
        {
            Contract.Requires(!string.IsNullOrEmpty(hostName));

            this.InitializeUniqueHeaderId();

            var entry = new IPHostEntry { HostName = hostName };
            
            // fill AddressList and aliases
            var addressList = new List<IPAddress>();
            var aliases = new List<string>();

            /*V4 addresses*/
            var responseInterNetwork = this.Query(hostName, QType.A);
            foreach (AnswerResourceRecord answerrr in responseInterNetwork.Answers)
            {
                if (answerrr.RRType == RecordType.A)
                {
                    addressList.Add(IPAddress.Parse(answerrr.Record.ToString()));
                    entry.HostName = answerrr.Name;
                }
                else
                {
                    if (answerrr.RRType == RecordType.CNAME)
                    {
                        aliases.Add(answerrr.Name);
                    }
                }
            }

            /*V6 addresses*/
            Response responseInterNetworkV6 = null;

            try
            {
                responseInterNetworkV6 = this.Query(hostName, QType.AAAA);
            }
            catch (SocketException socketException)
            {
                if (socketException.SocketErrorCode == SocketError.HostNotFound)
                {
                    /*Don't care*/
                }
                else
                {
                    this.Logger.Verbose(socketException, (int)EventId.NetSendReceiveFail);    
                }
            }
            if (responseInterNetworkV6 != null)
            {
                foreach (AnswerResourceRecord answerrr in responseInterNetworkV6.Answers)
                {
                    if (answerrr.RRType == RecordType.AAAA)
                    {
                        addressList.Add(IPAddress.Parse(answerrr.Record.ToString()));
                        entry.HostName = answerrr.Name;
                    }
                    else
                    {
                        if (answerrr.RRType == RecordType.CNAME)
                        {
                            aliases.Add(answerrr.Name);
                        }
                    }
                }
            }

            entry.AddressList = addressList.ToArray();
            entry.Aliases = aliases.ToArray();

            return entry;
        }

        /// <summary>
        /// Called when [verbose].
        /// </summary>
        /// <param name="args">
        /// The <see cref="VerboseEventArgs"/> instance containing the event data.
        /// </param>
        private void OnVerbose(VerboseEventArgs args)
        {
            if (null != this.VerboseEvent)
            {
                this.VerboseEvent(this, args);
            }
        }

        #endregion
    }
}